# Class for HTTP/HTTPS requests
=begin
Copyright (C) 2014 Enrique Rando

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

http://www.gnu.org/licenses/gpl-2.0.html
=end

require 'uri'
require 'net/http'
require 'net/https'
require 'base64'

class My_HTTP_client
	# Object creation
	#	Parameters:
	#		user_agent		User-Agente to use in requests
	#		proxy			Proxy name or address. Use nil for no proxy.
	#						By default, Ruby 2.1 uses HTTP_PROXY environment variable to set the proxy
	#		proxy_port		Proxy port
	#		proxy_user		Proxy username, if required
	#		proxy_password	Proxy password, if required.
	def initialize(user_agent="My HTTP Client", proxy=:ENV, proxy_port=nil, proxy_user=nil, proxy_password=nil)
		@user_agent = user_agent
		@proxy = proxy
		@proxy_port = proxy_port
		@proxy_user = proxy_user
		@proxy_password = proxy_password

		@target = nil
	end

	
	# Sets the server to make requests to
	# Parameters
	#		protocol	protocol to use: :http, :https	
	#		server		server to connect to
	#		port		port to use
	#		update_cookies	true if cookies are to be updated whit response data
	#		usr			user name for HTTP basic authentication. nil for no authentication
	#		pwd			password for HTTP basic authentication
	def set_server(protocol, server, port=:default, update_cookies=false, usr=nil, pwd="")
		raise "Unsupported protocol: " + protocol.to_s if not [:http, :https].include?(protocol)
		@update_cookies = update_cookies
		@cookie = ""

		# Determine port
		myport = (port === :default) ?
			case protocol
				when :http
					80
				when :https
					443
			end
			:
			port
			
		# Create object for requests
		@target = Net::HTTP.new(server, myport, @proxy, @proxy_port, @proxy_user, @proxy_password)
		@target.open_timeout = 10
		@target.read_timeout = 10
		@target.continue_timeout = 10
		@target.ssl_timeout = 10
		
		
		# Set authentication
		change_auth(usr, pwd)
		
		# Set HTTPS options
		if protocol === :https
			@target.use_ssl = true
			@target.verify_mode = OpenSSL::SSL::VERIFY_NONE
		end
	end

	# Changes current cookie
	# Parameter new_cookie		The new cookie
	def change_cookie(new_cookie)
		@cookie = new_cookie
	end
	
	# Changes User Agent
	# Parameter new_user_agent	The new user agent
	def change_user_agent(new_user_agent)
		@user_agent = new_user_agent
	end
	
	def change_auth(usr, pwd)
		@usr = usr
		@pwd = pwd
	end
	
	# Makes a GET request
	# Parameters
	#		path			URL path (without GET parameters)
	#		get_parameters	Hash of name=>value GET parameters
	#		headers			Hash of name=>value HTTP headers
	def get(path="/", get_parameters={}, headers = {}, retries=2)
		generic_request(path, get_parameters, "", headers, method=:get, retries)
	end


	# Makes a POST request
	#		path			URL path (without GET parameters)
	#		get_parameters	Hash of name=>value GET parameters
	#		post_data		String or Hash of name=>value GET parameters. 
	#						If a hash is provided, data is supposed to be a form
	#		headers			Hash of name=>value HTTP headers
	def post(path="/", get_parameters={}, post_data="", headers={}, retries=2)
		generic_request(path, get_parameters, post_data, headers, method=:post, retries)
	end

	# Makes a generic request
	#		path			URL path (without GET parameters)
	#		get_parameters	Hash of name=>value GET parameters
	#		post_data		String or Hash of name=>value GET parameters. 
	#						If a hash is provided, data is supposed to be a form
	#		headers			Hash of name=>value HTTP headers
	#		method			:post, :get
	def generic_request(path="/", get_parameters={}, post_data="", headers={}, method=:get, max_retries=2)
		mypath = full_url(path, get_parameters)
		myheaders = prepare_headers(headers)

		# Prepare post data if available
		mypost_data = post_data
		if post_data.is_a?(Hash)
			mypost_data = URI.encode_www_form(post_data)
			myheaders['Content-Type'] = 'application/x-www-form-urlencoded'
		end

		retries = 0
		response = nil
		finished = false
		while not finished
			begin
				case method
				when :post
					# Make the request
					request = Net::HTTP::Post.new(mypath)
					myheaders.each{|name,value| request[name] = value}
					reqest.set_form_data(mypost_data)
					request.basic_auth(@usr,@pwd) if not @usr.nil?
					
					# ... and send it
					response = @target.request(request)
					
					#response = @target.post(mypath, mypost_data, myheaders)
					finished = true
				when :get
					# Make the request
					request = Net::HTTP::Get.new(mypath)
					myheaders.each{|name,value| request[name] = value}
					request.basic_auth(@usr,@pwd) if not @usr.nil?
					
					# ... and send it
					response = @target.request(request)
					
					#response = @target.get(mypath, myheaders)
					finished = true
				else
					raise "Invalid method: #{method}"
				end
				
				# Save cookie
				if @update_cookies
					@cookie = prepare_cookie(response)
				end
			rescue Exception => e
				# Retry if max_retries not reached
				retries += 1
				response = nil
				finished = true if retries > max_retries
			end
		end

		return response	
	end
	
	# CLASS METHODS

	# Checks a response object for redirections
	# Parameter response 		the response
	# Returns array [is_there_a_redirection, redirection_type, url_to_redirect]
	# redirection_type may be :html or :http
	# url_requested		URL requested to get the response
	def self.detect_redirection(response, url_requested)
		return [false, nil, nil] if response.nil?
		
		# Check for HTTP redirection
		return [true, :http, url_redir(url_requested, response["location"])] if response.is_a?(Net::HTTPRedirection)
		
		# Check for HTML redirection
		[
			/<head>.*<meta\s[^>]*http-equiv\s*=\s*["']refresh["'].*?content\s*=\s*(?<sep>["']).*?url\s*=(?<url>.*?)\k<sep>/im,
			/<head>.*<meta\s[^>]*content\s*=\s*(?<sep>["']).*?url\s*=(?<url>.*?)\k<sep>[^>]*?http-equiv\s*["']refresh["']=/im,
			/<head>.*<meta\s[^>]*http-equiv\s*=\s*["']location["'].*?content\s*=\s*(?<sep>["'])(?<url>.*?)\k<sep>/im,
			/<head>.*<meta\s[^>]content\s*=\s*(?<sep>["'])(?<url>.*?)\k<sep>.*?http-equiv\s*=\s*["']location["']/im
		].each do |regexp|
			match = response.body.to_s.match(regexp)		
			if not match.nil?
				return [true, :html, url_redir(url_requested, match[:url])]
				break
			end
		end
		
		return [false, nil, nil]
	end
	
	# Makes correction to URLs to make sure they have a valid format
	# Parameter		url	The URLs
	def self.repair_url(url)
		return URI.escape(url.strip.gsub(/^['"](.*)["']$/,'\1').gsub(" ", "%20").gsub("[", "%5B").gsub("]", "%5D"))
	end

private

	# Calculates redirection URL
	# Takes into account relative Location values
	# Parameters:
	#	url_requested		The URL that was requested. Must be an absolute one
	#	redirected_to		The URL for redirection. May be relative
	
	def self.url_redir(url_requested, redirected_to)
		destination = My_HTTP_client.repair_url(redirected_to)
		
		uri = URI.parse(destination)
		if uri.scheme.nil? # Relative redirection. Same server and port
			old_uri = URI.parse(url_requested)
			uri.scheme = old_uri.scheme
			uri.host = old_uri.host
			uri.port = old_uri.port
			
			if uri.path[0] != "/" 
				uri.path = old_uri.path.sub(/\/[^\/]*$/,"") + "/" + uri.path
			end
			
			return uri.to_s
		else
			# Absolute redirection. Nothing to do
			return destination
		end
	end

	# Makes a full URL from URL path and get parameters
	# Parameters
	#	path			URL path
	#	get_parameters	GET parameters
	def full_url(path, get_parameters)
		mypath = path

		# Create the query string
		myparameters = URI.encode_www_form(get_parameters)
		if myparameters != ""
			mypath = mypath + (mypath.include?("?") ? "&" : "?") + myparameters
		end

		return mypath
	end

	# Prepares HTTP headers
	# Parameter header		User Provided Headers
	def prepare_headers(headers)
		myheaders = headers.clone
		if @cookie != ""
			myheaders['Cookie'] = @cookie
		end
		
		#if not @usr.nil? 
		#	myheaders['Authentication'] = "Basic " + Base64.encode64("#{@usr}:#{@pwd}")
		#end
		
		myheaders['User-Agent'] = @user_agent
		return myheaders
	end

	# Prepares the cookie from the response
	# Parameter response	The response
	def prepare_cookie(response)
		cookies = response.get_fields('set-cookie')
		return cookies.nil? ? "" : cookies.collect{|cookie| cookie.split(";")[0]}.join(";")
	end
end


